/************************************************

  lame.c - Lame module

  $Author: kouji.takao $
  created at: 2008-11-15

  Copyright (C) 2008       TAKAO Kouji

  $Id: lame.c 33 2008-11-20 05:05:31Z kouji.takao $

  Contact:
   - TAKAO Kouji <kouji at takao7 dot net> (current maintainer)

************************************************/

#ifdef RUBY_EXTCONF_H
#include RUBY_EXTCONF_H
#endif

#include "ruby.h"
#include <lame/lame.h>

/*
 * Document-class: Lame
 *
 * The Lame module provides interface for LAME.
 * LAME is an educational tool to be used for learning about MP3 encoding.
 * This module defines a number of methods to encode and decode between
 * PCM and MP3 or Ogg.
 *
 * LAME:: http://lame.sourceforge.net/index.php
 *
 * The encode methods are defined the Lame::Encoder class.
 * The decode methods are defined the Lame::Decoder class.
 *
 * == Encoding
 *
 * The PCM data (from STDIN) encode to MP3(to STDOUT). The PCM data must be
 * without WAVE header.
 *
 *   require "lame"
 *
 *   encoder = Lame::Encoder.new
 *   encoder.num_channels = 2
 *   encoder.sample_rate = 44100
 *   encoder.bit_rate = 128
 *   encoder.mpeg_mode = Lame::MpegMode::STEREO
 *   encoder.quality = 2
 *   f = ARGF
 *   while (pcm_data = f.read(1024 * 2))
 *     d = encoder.encode(pcm_data)
 *     print(d)
 *   end
 *   d = encoder.flush
 *   print(d)
 *
 * == Decoding
 *
 * Not supproted, yet.
 *
 * Documented by TAKAO Kouji <kouji at takao7 dot net>.
 */

static VALUE rb_mLame;
static VALUE rb_cEncoder;
static VALUE rb_cDecoder;
static VALUE rb_eLameError;
static VALUE rb_mMpegMode;
static VALUE rb_mPaddingType;

#define GFP(self, gfp) do {                             \
        Data_Get_Struct(self, lame_global_flags, gfp);  \
    } while (0)

#define LAME_RUBY_INT_ATTR_READER_BODY(func) do { \
        lame_global_flags *gfp;                   \
                                                  \
        GFP(self, gfp);                           \
        return INT2NUM(lame_get_##func(gfp));     \
    } while (0)

#define LAME_RUBY_INT_ATTR_WRITER_BODY(func) do  {      \
        lame_global_flags *gfp;                         \
        int ret;                                        \
                                                        \
        GFP(self, gfp);                                 \
        ret = lame_set_##func(gfp, NUM2INT(val));                       \
        if (ret < 0) {                                                  \
            rb_raise(rb_eLameError,                                     \
                     "lame_set_" #func "() returns %d.", ret);          \
        }                                                               \
        return val;                                                     \
    } while (0)

#define LAME_RUBY_BOOL_ATTR_READER_BODY(func) do {      \
        lame_global_flags *gfp;                         \
                                                        \
        GFP(self, gfp);                                 \
        return lame_get_##func(gfp) == 1 ? Qtrue : Qfalse;      \
    } while (0)

#define LAME_RUBY_BOOL_ATTR_WRITER_BODY(func) do {      \
        lame_global_flags *gfp;                         \
        int ret;                                        \
                                                        \
        GFP(self, gfp);                                 \
        ret = lame_set_##func(gfp, RTEST(val) ? 1 : 0); \
        if (ret < 0) {                                                  \
            rb_raise(rb_eLameError, "lame_set_" #func "() returns %d.", ret); \
        }                                                               \
        return val;                                                     \
    } while (0)

/*
 * call-seq: num_channels -> integer
 *
 * Gets number of channels in input.
 */
static VALUE
lame_ruby_encoder_get_num_channels(VALUE self)
{
    LAME_RUBY_INT_ATTR_READER_BODY(num_channels);
}

/*
 * call-seq: num_channels = integer
 *
 * Sets number of channels in input.
 *
 * Raises Lame::LameError if failed lame_set_num_channels().
 */
static VALUE
lame_ruby_encoder_set_num_channels(VALUE self, VALUE val)
{
    LAME_RUBY_INT_ATTR_WRITER_BODY(num_channels);
}

/*
 * call-seq: sample_rate -> integer
 *
 * Gets input sample rate in Hz.
 */
static VALUE
lame_ruby_encoder_get_sample_rate(VALUE self)
{
    LAME_RUBY_INT_ATTR_READER_BODY(in_samplerate);
}

/*
 * call-seq: sample_rate = integer
 *
 * Sets input sample rate in Hz.
 *
 * Raises Lame::LameError if failed lame_set_in_samplerate().
 */
static VALUE
lame_ruby_encoder_set_sample_rate(VALUE self, VALUE val)
{
    LAME_RUBY_INT_ATTR_WRITER_BODY(in_samplerate);
}

/*
 * call-seq: bit_rate -> integer
 *
 * Gets the output bit rate in Kbps.
 */
static VALUE
lame_ruby_encoder_get_bit_rate(VALUE self)
{
    LAME_RUBY_INT_ATTR_READER_BODY(brate);
}

/*
 * call-seq: bit_rate = integer
 *
 * Sets the output bit rate in Kbps.
 *
 * Raises Lame::LameError if failed lame_set_brate().
 */
static VALUE
lame_ruby_encoder_set_bit_rate(VALUE self, VALUE val)
{
    LAME_RUBY_INT_ATTR_WRITER_BODY(brate);
}

/*
 * call-seq: mpeg_mode -> integer
 *
 * Gets the MPEG mode. See the constants under the Lame::MpegMode module.
 */
static VALUE
lame_ruby_encoder_get_mpeg_mode(VALUE self)
{
    LAME_RUBY_INT_ATTR_READER_BODY(mode);
}

/*
 * call-seq: mpeg_mode = integer
 *
 * Sets the MPEG mode. See the constants under the Lame::MpegMode module.
 *
 *   require "lame"
 *
 *   encoder = Lame::Encoder.new
 *   encoder.mpeg_mode = Lame::MpegMode::MONO
 *
 * Raises Lame::LameError if failed lame_set_mode().
 */
static VALUE
lame_ruby_encoder_set_mpeg_mode(VALUE self, VALUE val)
{
    LAME_RUBY_INT_ATTR_WRITER_BODY(mode);
}

/*
 * call-seq: quality -> integer
 *
 * Gets the quality. 
 */
static VALUE
lame_ruby_encoder_get_quality(VALUE self)
{
    LAME_RUBY_INT_ATTR_READER_BODY(quality);
}

/*
 * call-seq: quality = integer
 *
 * Sets the quality. See lame_set_quality() in lame/lame.h.
 *
 * Raises Lame::LameError if failed lame_set_quality().
 */
static VALUE
lame_ruby_encoder_set_quality(VALUE self, VALUE val)
{
    LAME_RUBY_INT_ATTR_WRITER_BODY(quality);
}

/*
 * call-seq: padding_type -> integer
 *
 * Gets the padding type. See the constants under Lame::PaddingType
 * module.
 */
static VALUE
lame_ruby_encoder_get_padding_type(VALUE self)
{
    LAME_RUBY_INT_ATTR_READER_BODY(padding_type);
}

/*
 * call-seq: padding_type = integer
 *
 * <b>No effect this method</b>.
 * Sets the padding type. See the constants under Lame::PaddingType
 * module.
 */
static VALUE
lame_ruby_encoder_set_padding_type(VALUE self, VALUE val)
{
    LAME_RUBY_INT_ATTR_WRITER_BODY(padding_type);
}

/*
 * call-seq: ogg -> boolean
 *
 * Gets whether output format. true is Ogg, false is MP3.
 * <b>Now, returns only false</b>.
 */
static VALUE
lame_ruby_encoder_get_ogg(VALUE self)
{
    LAME_RUBY_BOOL_ATTR_READER_BODY(ogg);
}

/*
 * call-seq: mpeg_version -> integer
 *
 * Gets the MPEG version. 0 is MPEG-2, 1 is MPEG 1. (2 is MPEG-2.5.)
 */
static VALUE
lame_ruby_encoder_get_mpeg_version(VALUE self)
{
    LAME_RUBY_INT_ATTR_READER_BODY(version);
}

/*
 * call-seq: delay -> integer
 *
 * Gets the encoder delay.
 */
static VALUE
lame_ruby_encoder_get_delay(VALUE self)
{
    LAME_RUBY_INT_ATTR_READER_BODY(encoder_delay);
}

/*
 * call-seq: padding -> integer
 *
 * Gets the padding appended to the input to make sure decoder can
 * fully decode all input.
 * See lame_get_encoder_padding() in lame/lame.h.
 */
static VALUE
lame_ruby_encoder_get_padding(VALUE self)
{
    LAME_RUBY_INT_ATTR_READER_BODY(encoder_padding);
}

/*
 * call-seq: frame_size -> integer
 *
 * Gets size of MPEG frame.
 */
static VALUE
lame_ruby_encoder_get_frame_size(VALUE self)
{
    LAME_RUBY_INT_ATTR_READER_BODY(framesize);
}

static void
lame_ruby_free(lame_global_flags *gfp)
{
    if (gfp != NULL) {
        lame_close(gfp);
    }
}

static VALUE
lame_ruby_s_alloc(VALUE klass)
{
    return Data_Wrap_Struct(klass, 0, lame_ruby_free, 0);
}

/*
 * call-seq:
 *   encoder = Lame::Encoder.new(options = nil)
 *
 * Creates the Lame::Encoder instance.
 *
 *   require "lame"
 *
 *   options = {
 *     :num_channels => 1,
 *     :sample_rate => 22050,
 *     :mpeg_mode => Lame::MpegMode::MONO,
 *     :bit_rate => 32,
 *   }
 *   encoder = Lame::Encoder.new(options)
 *
 * Raises Lame::LameError if failed lame_init().
 */
static VALUE
lame_ruby_encoder_initialize(int argc, VALUE *argv, VALUE self)
{
    lame_global_flags *gfp;
    VALUE options;

    gfp = lame_init();
    if (gfp == NULL) {
        rb_raise(rb_eLameError, "lame_init() returns NULL.");
    }
    lame_ruby_free(DATA_PTR(self));
    DATA_PTR(self) = gfp;
    rb_iv_set(self, "@encoding", Qfalse);
    if (rb_scan_args(argc, argv, "01", &options) == 1) {
        VALUE val;
        struct {
            char *name;
            VALUE (*setter)(VALUE self, VALUE val);
        } option_desc[] = {
            { "num_channels", lame_ruby_encoder_set_num_channels },
            { "sample_rate", lame_ruby_encoder_set_sample_rate },
            { "bit_rate", lame_ruby_encoder_set_bit_rate },
            { "mpeg_mode", lame_ruby_encoder_set_mpeg_mode },
            { "quality", lame_ruby_encoder_set_quality },
            { "padding_type", lame_ruby_encoder_set_padding_type },
        }, *p, *p_end;

        for (p = option_desc,
                 p_end = p + sizeof(option_desc) / sizeof(option_desc[0]);
             p != p_end; p++) {
            val = rb_hash_aref(options, ID2SYM(rb_intern(p->name)));
            if (!NIL_P(val)) {
                (*p->setter)(self, val);
            }
        }
    }
    return self;
}

#ifdef DEBUG
static void
print_buffer_c(char *prefix, unsigned char *buf, int len)
{
    unsigned char *p, *p_end;
    
    fprintf(stderr, prefix);
    for (p = buf, p_end = p + len; p != p_end; p++) {
        fprintf(stderr, " %02x", *p);
    }
    fprintf(stderr, "\n");
}

static void
print_buffer(char *prefix, VALUE buf)
{
    print_buffer_c(prefix, (unsigned char *) RSTRING_PTR(buf), RSTRING_LEN(buf));
}
#endif /* #ifdef DEBUG */

static int
calc_mp3_buffer_size(int num_samples)
{
    return (int) (1.25 * num_samples + 7200);
}

/*
 * call-seq: encode(left_pcm_data, right_pcm_data = nil) -> string
 *
 * Inputs PCM data(left_pcm_data and right_pcm_data),
 * outputs (maybe) mp3 frames.
 * See lame_encode_buffer() and lame_encode_buffer_interleaved() in lame/lame.h.
 *
 * Raises Lame::LameError, if decoding or failed
 * lame_encode_buffer{,_interleaved}().
 */
static VALUE
lame_ruby_encoder_encode(int argc, VALUE *argv, VALUE self)
{
    lame_global_flags *gfp;
    VALUE left_pcm_data;
    VALUE right_pcm_data;
    int ret;
    static int num_channels;
    int num_samples;
    int mp3_buffer_size;
    VALUE mp3_buffer;
        
    rb_scan_args(argc, argv, "11", &left_pcm_data, &right_pcm_data);
    GFP(self, gfp);
    if (!RTEST(rb_iv_get(self, "@encoding"))) {
        rb_iv_set(self, "@encoding", Qtrue);
        ret = lame_init_params(gfp);
        if (ret < 0) {
            rb_raise(rb_eLameError, "lame_init_params() returns %d.", ret);
        }
        num_channels = NUM2INT(lame_ruby_encoder_get_num_channels(self));
    }
    if (num_channels == 2 && NIL_P(right_pcm_data) &&
        RSTRING_LEN(left_pcm_data) % (sizeof(short int) * num_channels) != 0) {
        rb_raise(rb_eLameError, "invalid PCM data length: %d, "
                 "PCM data length must be a multiple of %d.",
                 RSTRING_LEN(left_pcm_data), sizeof(short int) * num_channels);
    }
    if (num_channels == 2 && NIL_P(right_pcm_data)) {
        num_samples =
            RSTRING_LEN(left_pcm_data) / sizeof(short int) / num_channels;
    }
    else {
        num_samples = RSTRING_LEN(left_pcm_data) / sizeof(short int);
    }
    mp3_buffer_size = calc_mp3_buffer_size(num_samples);
    mp3_buffer = rb_str_new(NULL, mp3_buffer_size);
    if (num_channels == 1) {
        ret = lame_encode_buffer
            (gfp, (short int *) RSTRING_PTR(left_pcm_data), 0, num_samples,
             (unsigned char *) RSTRING_PTR(mp3_buffer), mp3_buffer_size);
        if (ret < 0) {
            rb_raise(rb_eLameError,
                     "lame_encode_buffer() returns %d.", ret);
        }
    }
    else {
        if (NIL_P(right_pcm_data)) {
            ret = lame_encode_buffer_interleaved
                (gfp, (short int *) RSTRING_PTR(left_pcm_data), num_samples,
                 (unsigned char *) RSTRING_PTR(mp3_buffer), mp3_buffer_size);
            if (ret < 0) {
                rb_raise(rb_eLameError,
                         "lame_encode_buffer_interleaved() returns %d.", ret);
            }
        }
        else {
            ret = lame_encode_buffer
                (gfp, (short int *) RSTRING_PTR(left_pcm_data),
                 (short int *) RSTRING_PTR(right_pcm_data),
                 num_samples,
                 (unsigned char *) RSTRING_PTR(mp3_buffer), mp3_buffer_size);
            if (ret < 0) {
                rb_raise(rb_eLameError,
                         "lame_encode_buffer() returns %d.", ret);
            }
        }
    }
    rb_str_resize(mp3_buffer, ret);
    return mp3_buffer;
}

/*
 * call-seq: flush(nogap = false) -> string
 *
 * Flushes the intenal PCM buffers, padding with 0's to make sure the
 * final frame is complete, and then flush the internal MP3 buffers,
 * and thus may return a final few mp3 frames.
 *
 * See lame_encode_flush() and lame_encode_flush_nogap() in lame/lame.h.
 *
 * Raises Lame::LameError, if decoding or failed lame_encode_flush()
 * or lame_encode_flush_nogap().
 */
static VALUE
lame_ruby_encoder_flush(int argc, VALUE *argv, VALUE self)
{
    lame_global_flags *gfp;
    VALUE nogap;
    int ret;
    VALUE res;
    int mp3_buffer_size;
    VALUE mp3_buffer;
    /*unsigned char mp3_buffer[1024];*/

    if (rb_scan_args(argc, argv, "01", &nogap) == 0) {
        nogap = Qfalse;
    }
    GFP(self, gfp);
    if (!RTEST(rb_iv_get(self, "@encoding"))) {
        rb_iv_set(self, "@encoding", Qtrue);
        ret = lame_init_params(gfp);
        if (ret < 0) {
            rb_raise(rb_eLameError, "lame_init_params() returns %d.", ret);
        }
    }
    mp3_buffer_size = calc_mp3_buffer_size(lame_get_num_samples(gfp));
    mp3_buffer = rb_str_new(NULL, mp3_buffer_size);
    res = rb_str_new(NULL, 0);
    do {
        if (RTEST(nogap)) {
            ret = lame_encode_flush_nogap
                (gfp, (unsigned char *) RSTRING_PTR(mp3_buffer),
                 mp3_buffer_size);
        }
        else {
            ret = lame_encode_flush
                (gfp, (unsigned char *) RSTRING_PTR(mp3_buffer),
                 mp3_buffer_size);
        }
        if (ret < 0) {
            rb_raise(rb_eLameError,
                     "lame_encode_flush() returns %d.", ret);
        }
        rb_str_cat(res, RSTRING_PTR(mp3_buffer), ret);
    } while (ret > 0);
    rb_iv_set(self, "@encoding", Qfalse);
    return res;
}

/*
 * call-seq: sample_rate -> integer
 *
 * Gets output sample rate in Hz.
 */
static VALUE
lame_ruby_decoder_get_sample_rate(VALUE self)
{
    LAME_RUBY_INT_ATTR_READER_BODY(out_samplerate);
}

/*
 * call-seq: sample_rate = integer
 *
 * Sets output sample rate in Hz.
 *
 * Raises Lame::LameError if failed lame_set_out_samplerate().
 */
static VALUE
lame_ruby_decoder_set_sample_rate(VALUE self, VALUE val)
{
    LAME_RUBY_INT_ATTR_WRITER_BODY(out_samplerate);
}

/*
 * call-seq:
 *   decoder = Lame::Decoder.new(options = nil)
 *
 * Creates the Lame::Decoder instance.
 *
 * Raises Lame::LameError if failed lame_init().
 */
static VALUE
lame_ruby_decoder_initialize(int argc, VALUE *argv, VALUE self)
{
    lame_global_flags *gfp;
    VALUE options;

    gfp = lame_init();
    if (gfp == NULL) {
        rb_raise(rb_eLameError, "lame_init() returns NULL.");
    }
    lame_ruby_free(DATA_PTR(self));
    DATA_PTR(self) = gfp;
    if (rb_scan_args(argc, argv, "01", &options) == 1) {
        VALUE val;
        struct {
            char *name;
            VALUE (*setter)(VALUE self, VALUE val);
        } option_desc[] = {
            { "sample_rate", lame_ruby_decoder_set_sample_rate },
        }, *p, *p_end;

        for (p = option_desc,
                 p_end = p + sizeof(option_desc) / sizeof(option_desc[0]);
             p != p_end; p++) {
            val = rb_hash_aref(options, rb_intern(p->name));
            if (!NIL_P(val)) {
                (*p->setter)(self, val);
            }
        }
    }
    return self;
}

void Init_lame()
{
    rb_mLame = rb_define_module("Lame");

    /* The Lame::LameError exception class. */
    rb_eLameError =
        rb_define_class_under(rb_mLame, "LameError", rb_eStandardError);

    /* LAME version in a string. Same as get_lame_version(). */
    rb_define_const(rb_mLame, "VERSION", rb_str_new2(get_lame_version()));

    /* The Lame::Encoder class defines encode from PCM to MP3 or Ogg methods. */
    rb_cEncoder = rb_define_class_under(rb_mLame, "Encoder", rb_cObject);
    rb_define_alloc_func(rb_cEncoder, lame_ruby_s_alloc);
    rb_define_method(rb_cEncoder, "initialize",
                     lame_ruby_encoder_initialize, -1);

    rb_define_attr(rb_cEncoder, "encoding", 1, 0);
    
    rb_define_method(rb_cEncoder, "num_channels",
                     lame_ruby_encoder_get_num_channels, 0);
    rb_define_method(rb_cEncoder, "num_channels=",
                     lame_ruby_encoder_set_num_channels, 1);

    rb_define_method(rb_cEncoder, "sample_rate",
                     lame_ruby_encoder_get_sample_rate, 0);
    rb_define_method(rb_cEncoder, "sample_rate=",
                     lame_ruby_encoder_set_sample_rate, 1);
    rb_define_method(rb_cEncoder, "bit_rate",
                     lame_ruby_encoder_get_bit_rate, 0);
    rb_define_method(rb_cEncoder, "bit_rate=",
                     lame_ruby_encoder_set_bit_rate, 1);
    rb_define_method(rb_cEncoder, "mpeg_mode",
                     lame_ruby_encoder_get_mpeg_mode, 0);
    rb_define_method(rb_cEncoder, "mpeg_mode=",
                     lame_ruby_encoder_set_mpeg_mode, 1);
    rb_define_method(rb_cEncoder, "quality",
                     lame_ruby_encoder_get_quality, 0);
    rb_define_method(rb_cEncoder, "quality=",
                     lame_ruby_encoder_set_quality, 1);
    rb_define_method(rb_cEncoder, "mpeg_version",
                     lame_ruby_encoder_get_mpeg_version, 0);
    rb_define_method(rb_cEncoder, "delay",
                     lame_ruby_encoder_get_delay, 0);
    rb_define_method(rb_cEncoder, "padding",
                     lame_ruby_encoder_get_padding, 0);
    rb_define_method(rb_cEncoder, "frame_size",
                     lame_ruby_encoder_get_frame_size, 0);
    rb_define_method(rb_cEncoder, "padding_type",
                     lame_ruby_encoder_get_padding_type, 0);
    rb_define_method(rb_cEncoder, "padding_type=",
                     lame_ruby_encoder_set_padding_type, 1);
    rb_define_method(rb_cEncoder, "ogg",
                     lame_ruby_encoder_get_ogg, 0);

    rb_define_method(rb_cEncoder, "encode",
                     lame_ruby_encoder_encode, -1);
    rb_define_method(rb_cEncoder, "flush",
                     lame_ruby_encoder_flush, -1);

    /* The Lame::Decoder class defines decode from MP3 or Ogg to PCM methods. */
    rb_cDecoder = rb_define_class_under(rb_mLame, "Decoder", rb_cObject);
    rb_define_alloc_func(rb_cDecoder, lame_ruby_s_alloc);
    rb_define_method(rb_cDecoder, "initialize",
                     lame_ruby_decoder_initialize, -1);
    rb_define_method(rb_cDecoder, "sample_rate",
                     lame_ruby_decoder_get_sample_rate, 0);
    rb_define_method(rb_cDecoder, "sample_rate=",
                     lame_ruby_decoder_set_sample_rate, 1);

    /* The constants under the LAME::MpegMode module are used Lame#mpeg_mode= method. */
    rb_mMpegMode = rb_define_module_under(rb_mLame, "MpegMode");
    rb_define_const(rb_mMpegMode, "STEREO", INT2NUM(STEREO));
    rb_define_const(rb_mMpegMode, "JOINT_STEREO", INT2NUM(JOINT_STEREO));
    rb_define_const(rb_mMpegMode, "MONO", INT2NUM(MONO));
    rb_define_const(rb_mMpegMode, "NOT_SET", INT2NUM(NOT_SET));
    
    /* The constants under the LAME::PaddingType module are used Lame#padding_type= method.*/
    rb_mPaddingType = rb_define_module_under(rb_mLame, "PaddingType");
    rb_define_const(rb_mPaddingType, "NO", INT2NUM(PAD_NO));
    rb_define_const(rb_mPaddingType, "ALL", INT2NUM(PAD_ALL));
    rb_define_const(rb_mPaddingType, "ADJUST", INT2NUM(PAD_ADJUST));
}
